/******************************************************************************************************
Copyright (C) 2013 Mestrelab Research S.L. All rights reserved.

This file is part of the MNova scripting toolkit.

Authorized users of MNova Software may use this file freely, but this file is provided AS IS
with NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
*****************************************************************************************************/

/*globals Dir, Application, MessageBox, qsTr, settings, CommonSlots, File, TextStream, ProgressDialog, serialization, MassItem, Str, MnUi, print*/
/*jslint plusplus: true, indent: 4*/

function ChromatogramSubtractor(aBackFiles, aForeFiles, aRefIons, aSingleMode, aPrecision, aResDir) {
	"use strict";
	this.aBackFiles = aBackFiles;
	this.aForeFiles = aForeFiles;
	this.aRefIons = aRefIons;
	this.aSingleMode = aSingleMode;
	this.aPrecision = aPrecision;
	this.aResDir = aResDir;
	this.methodsTable = [];
	this.impuritiesTable = [];
	this.log = "";

}

function ChromatogramSubtraction() {
	"use strict";

	//Version check
	if (Application.version.revision < 8731) {
		MessageBox.critical(qsTr("You are running {0} {1}.\nThis script function can be run with MNova version 7.0.2 and higher.").format(Application.name, Application.version.full));
		return;
	}

	var backDir, foreDir, outputDir, refIons, items, numIons, i, range, str, chromatogramSubtractor,
	//Constants
		FILENAME,
		RESULTSDIR,
		CHECKABLE = 48, //Enabled(32) and Checkable(16)
		UNCHECKED = 0,
		ENABLED = 32,
		CHECKED = 2,
		COLS = 3,  //Cols in main dialog table widget
	//Registry values
		BackFolderKey = "Chromatogram Subtraction/Backfolder Directory",
		ForeFolderKey = "Chromatogram Subtraction/Forefolder Directory",
		OutputFolderKey = "Chromatogram Subtraction/Outputfolder Directory",
		BackMaskKey = "Chromatogram Subtraction/Backfolder Mask",
		ForeMaskKey = "Chromatogram Subtraction/Forefolder Mask",
		OptionKey = "Chromatogram Subtraction/Option",
		PrecisionKey = "Chromatogram Subtraction/Precision",
	//Dialogs
		diag = Application.loadUiFile("ricares:ChromatogramSubtraction.ui"),
		dialog = Application.loadUiFile("ricares:ChromatogramSubtraction_ReferenceIons.ui"),
	//Initial values
		backDirName = settings.value(BackFolderKey, Dir.home()),
		foreDirName = settings.value(ForeFolderKey, Dir.home()),
		outputDirName = settings.value(OutputFolderKey, Dir.home()),
		backFiles,
		foreFiles,
		precision = settings.value(PrecisionKey, 1),
		showDialog = true,
		msgFolderNotExists = qsTr("Folder {0} does not exist.");

	//-----------------------HANDLERS SECTION-------------------------
	function cleanDialog() {
		dialog.widgets.leFormula.text = "";
		dialog.widgets.leBackFrom.text = "";
		dialog.widgets.leBackTo.text = "";
		dialog.widgets.leForeFrom.text = "";
		dialog.widgets.leForeTo.text = "";
	}

	function refreshTable() {
		var items = diag.widgets.twAll.items;
		diag.widgets.twAll.items = items;
	}

	function getDataFromDialog(row, newRow) {
		if (dialog.widgets.leFormula.text !== "" && dialog.widgets.leBackFrom.text !== "" && dialog.widgets.leBackTo.text !== "" && dialog.widgets.leForeFrom.text !== "" && dialog.widgets.leForeTo.text !== "") {
			if (newRow) {
				diag.widgets.twAll.insertRow("");
			}
			var items = diag.widgets.twAll.items;
			items[row][0].text = dialog.widgets.leFormula.text;
			items[row][1].text = "[" + dialog.widgets.leBackFrom.text + "," + dialog.widgets.leBackTo.text + "]";
			items[row][2].text = "[" + dialog.widgets.leForeFrom.text + "," + dialog.widgets.leForeTo.text + "]";
			items[row][0].flags = CHECKABLE;
			items[row][1].flags = ENABLED;
			items[row][2].flags = ENABLED;
			diag.widgets.twAll.items = items;
			refreshTable();
			
			return true;
		} else {
			MessageBox.warning(qsTr("Invalid data"));
			return false;
		}
	}

	function btNewClickHandler() {
		diag.widgets.btNew.clicked.connect(function () {
			cleanDialog();
			if (dialog.exec() === 1) {
				getDataFromDialog(0, true);
			}
		});
	}

	function btBackMSHandler() {
		diag.widgets.btBackMS.clicked.connect(diag.widgets.leBackFolder, CommonSlots.onOpenDir);
	}

	function btOutputHandler() {
		diag.widgets.btOutput.clicked.connect(diag.widgets.leOutputFolder, CommonSlots.onOpenDir);
	}

	function btForeMSHandler() {
		diag.widgets.btForeMS.clicked.connect(diag.widgets.leForeFolder, CommonSlots.onOpenDir);
	}


	function btSelectNoneClickHandler() {
		diag.widgets.btSelectNone.clicked.connect(function () {
			var i,
				items = diag.widgets.twAll.items;
			for (i = 0; i < items.length; i++) {
				items[i][0].checkState = UNCHECKED;
				diag.widgets.twAll.items = items;
			}
		});
	}

	function btSelectAllClickHandler() {
		diag.widgets.btSelectAll.clicked.connect(function () {
			var i,
				items = diag.widgets.twAll.items;
			for (i = 0; i < items.length; i++) {
				items[i][0].checkState = CHECKED;
				diag.widgets.twAll.items = items;
			}
		});
	}


	function btDeleteClickHandler() {
		diag.widgets.btDelete.clicked.connect(function () {
			var noneSelected = true,
				items = diag.widgets.twAll.items,
				total = items.length,
				i = 0;

			while (i < total) {
				if (items[i][0].checkState === CHECKED) {
					diag.widgets.twAll.removeRow(i);
					items = diag.widgets.twAll.items;
					noneSelected = false;
					total--;
				} else {
					i++;
				}
			}
			if (noneSelected) {
				MessageBox.warning(qsTr("No elements selected"));
			}
		});
	}

	function dblClickHandler() {
		diag.widgets.twAll.cellDoubleClicked.connect(function (row, col) {
			var str,
				items = diag.widgets.twAll.items;
			dialog.widgets.leFormula.text = items[row][0].text;

			str = items[row][1].text;
			str = str.slice((str.indexOf("[", 0) + 1), str.indexOf(","));
			dialog.widgets.leBackFrom.text = str;

			str = items[row][1].text;
			str = str.slice((str.indexOf(",", 0) + 1), str.indexOf("]"));
			dialog.widgets.leBackTo.text = str;

			str = items[row][2].text;
			str = str.slice((str.indexOf("[", 0) + 1), str.indexOf(","));
			dialog.widgets.leForeFrom.text = str;

			str = items[row][2].text;
			str = str.slice((str.indexOf(",", 0) + 1), str.indexOf("]"));
			dialog.widgets.leForeTo.text = str;

			if (dialog.exec() === 1) {
				getDataFromDialog(row, false);
				cleanDialog();
			}
		});
	}

	//-----------------------FUNCTIONS SECTION-------------------------
	function loadIonFile() {
		var s, all, len, row, col, data, i, items, j,
			f = new File(FILENAME);
		f.open(File.ReadOnly);
		s = new TextStream(f);
		all = s.readAll().toString();
		len = all.length;
		row = 0;
		col = 0;
		data = [[]];
		all.trim();
		all = all.replace(/\r\n/g, "|");
		all = all.replace(/\t/g, "¬");

		data[row][col] = "";
		for (i = 0; i < all.length; i++) {
			if (all[i] === "¬") {
				col++;
				data[row][col] = "";
			} else if (all[i] === "|") {
				row++;
				col = 0;
				data.push([]);
				data[row][col] = "";
			} else {
				data[row][col] += all[i];
			}
		}
		data.pop([]);

		for (i = 0; i < data.length; i++) {
			if (data[i] !== undefined) {
				diag.widgets.twAll.insertRow("");
			}
		}
		items = diag.widgets.twAll.items;

		for (i = 0; i < data.length; i++) {
			for (j = 0; j < COLS; j++) {
				items[i][j].text = data[i][j];
				if (j === 0) {
					items[i][j].flags = CHECKABLE;
				} else {
					items[i][j].flags = ENABLED;
				}
			}
		}

		diag.widgets.twAll.items = items;
		refreshTable();
	}

	function saveIonFile() {
		var rows = diag.widgets.twAll.items.length, i, j, f, s,
			strOutput = "",
			items = diag.widgets.twAll.items;

		if (rows > 0) {
			for (i = 0; i < rows; i++) {
				for (j = 0; j < COLS; j++) {
					strOutput += items[i][j].text;
					if (j < (COLS - 1)) {
						strOutput += "\t";
					}
				}
				strOutput += "\r\n";
			}

			f = new File(FILENAME);
			f.open(File.WriteOnly);
			s = new TextStream(f);
			s.write(strOutput);
			s.flush();
			f.close();
		}
	}

	diag.widgets.leBackMask.text = settings.value(BackMaskKey, "");
	diag.widgets.leForeMask.text = settings.value(ForeMaskKey, "");
	diag.widgets.cbSingleBackSpec.checked = settings.value(OptionKey, false);
	diag.widgets.leBackFolder.text = backDirName;
	diag.widgets.leForeFolder.text = foreDirName;
	diag.widgets.leOutputFolder.text = outputDirName;
	diag.widgets.sbPrecision.value = settings.value(PrecisionKey, 1);

	//Handlers
	btNewClickHandler();
	btDeleteClickHandler();
	dblClickHandler();
	btSelectAllClickHandler();
	btSelectNoneClickHandler();
	btBackMSHandler();
	btForeMSHandler();
	btOutputHandler();

	//---------------------Load Settings--------------------------

	loadIonFile();

	while (showDialog) {

		if (!diag.exec()) {
			return;
		}

		precision = diag.widgets.sbPrecision.value;
		backDirName = diag.widgets.leBackFolder.text;

		backDir = new Dir(backDirName);
		if (!backDirName || !backDir.exists) {
			MessageBox.critical(msgFolderNotExists.format(backDirName));
			diag.widgets.leBackFolder.setFocus();
		} else {
			foreDirName = diag.widgets.leForeFolder.text;
			foreDir = new Dir(foreDirName);
			if (!backDirName || !foreDir.exists) {
				MessageBox.critical(msgFolderNotExists.format(foreDirName));
				diag.widgets.leForeFolder.setFocus();
			} else {
				outputDirName = diag.widgets.leOutputFolder.text;
				outputDir = new Dir(outputDirName);
				if (!outputDirName || !outputDir.exists) {
					MessageBox.critical(msgFolderNotExists.format(outputDirName));
					diag.widgets.leOutputFolder.setFocus();
				} else {
					if (diag.widgets.leBackMask.text.length) {
						backFiles = backDir.entryListAdv(diag.widgets.leBackMask.text);
					} else {
						backFiles  = undefined;
					}

					if (diag.widgets.leForeMask.text.length) {
						foreFiles = foreDir.entryListAdv(diag.widgets.leForeMask.text);
					} else {
						foreFiles  = undefined;
					}

					if (diag.widgets.cbSingleBackSpec.checked && backFiles.length > 1) {
						MessageBox.critical(qsTr("Found more than one background spectrum"));
						diag.widgets.leBackMask.setFocus();
					} else {
						if (!diag.widgets.cbSingleBackSpec.checked && backFiles.length !== foreFiles.length) {
							MessageBox.critical(qsTr("The number of  background and foreground spectra must be the same"));
							diag.widgets.leBackMask.setFocus();
						} else {
							if (backFiles.length === 0) {
								MessageBox.critical(qsTr("No background spectrum found"));
								diag.widgets.leBackMask.setFocus();
							} else {
								if (foreFiles.length === 0) {
									MessageBox.critical(qsTr("No foreground spectrum found"));
									diag.widgets.leForeMask.setFocus();
								} else {
									showDialog = false;
								}
							}
						}
					}
				}
			}
		}
	}

	//Save values to registry
	settings.setValue(BackFolderKey, backDirName);
	settings.setValue(ForeFolderKey, foreDirName);
	settings.setValue(OutputFolderKey, outputDirName);
	settings.setValue(BackMaskKey, diag.widgets.leBackMask.text);
	settings.setValue(ForeMaskKey, diag.widgets.leForeMask.text);
	settings.setValue(OptionKey, diag.widgets.cbSingleBackSpec.checked);
	settings.setValue(PrecisionKey, diag.widgets.sbPrecision.value);

	//Save changes on the ion definition file
	saveIonFile();

	refIons = [];
	items = diag.widgets.twAll.items;
	numIons = 0;

	for (i = 0; i < items.length; i++) {
		if (items[i][0].checked) {
			refIons[numIons] = {};
			refIons[numIons].formula = items[i][0].text;
			range = items[i][1].text;

			str = range.slice((range.indexOf("[", 0) + 1), range.indexOf(","));
			refIons[numIons].background = {};
			refIons[numIons].background.from = str;
			str = range.slice((range.indexOf(",", 0) + 1), range.indexOf("]"));
			refIons[numIons].background.to = str;

			range = items[numIons][2].text;
			str = range.slice((range.indexOf("[", 0) + 1), range.indexOf(","));
			refIons[numIons].foreground = {};
			refIons[numIons].foreground.from = str;
			str = range.slice((range.indexOf(",", 0) + 1), range.indexOf("]"));
			refIons[numIons].foreground.to = str;
			numIons++;
		}
	}
	RESULTSDIR = outputDirName;
	FILENAME = outputDirName + "/ReferenceIons.txt"; //File where the list of ions is stored
	chromatogramSubtractor = new ChromatogramSubtractor(backFiles, foreFiles, refIons, diag.widgets.cbSingleBackSpec.checked, precision, RESULTSDIR);
	chromatogramSubtractor.processAllSpectra();
}

ChromatogramSubtractor.prototype.processAllSpectra = function () {
	"use strict";
	var i, j, method, vector, backPage, dw, bkgMSItem, frgMSItem, RTIndex, tic, aux, max, peaks,
		fileName,
		errors = "",
		aBackFiles = this.aBackFiles,
		aForeFiles = this.aForeFiles,
		aRefIons = this.aRefIons,
		aSingleMode = this.aSingleMode,
		aResDir = this.aResDir,
		methodsTable = this.methodsTable,
		progress = new ProgressDialog(),
		startDate =  Str.date2ISOString(new Date()),
		startDateDir = startDate.replace(new RegExp("\\:", 'g'), "."),
		completeDate,
		dir,
		f,
		s,
		tempDir = new Dir(aResDir),
		methods = 0;


	tempDir.mkdir(startDateDir);
	tempDir.cd(startDateDir);
	aResDir = tempDir.absPath;

	progress.minimumDuration = 0;
	progress.setRange(0, aForeFiles.length);
	progress.show();

	try {
		for (i = 0; i < aForeFiles.length && !progress.wasCanceled; i++) {
			progress.value = i;
			progress.labelText = qsTr("Subtracting {0} of {1}...").format(i + 1, aForeFiles.length);
			method = {};
			vector = [];

			method.vector = vector;
			Application.mainWindow.newDocument();

			if (aSingleMode) {
				serialization.open(aBackFiles[0]);
			} else {
				serialization.open(aBackFiles[i]);
			}

			backPage = Application.mainWindow.activeDocument.curPage();
			serialization.open(aForeFiles[i]);
			dw = Application.mainWindow.activeDocument;

			bkgMSItem = new MassItem(dw.item(0, "Mass Spectrum"));
			if (!bkgMSItem.isValid()) {
				MessageBox.critical(qsTr("Two mass items: background and foreground are needed."), MessageBox.Ok);
				return;
			}

			frgMSItem = new MassItem(dw.item(1, "Mass Spectrum"));
			if (!frgMSItem.isValid()) {
				MessageBox.critical(qsTr("Two mass items: background and foreground are needed"), MessageBox.Ok);
				return;
			}

			if (aRefIons.length === 0) {
				frgMSItem.backgroundSubtract(bkgMSItem, 0, 0, 0, 0);
			} else {
				RTIndex = frgMSItem.alignForBackgroundSubtraction(bkgMSItem, 0, 0, 0, 0, aRefIons);
				if (RTIndex !== undefined) {
					frgMSItem.backgroundSubtract(bkgMSItem, 0, 0, 0, 0, RTIndex);
				} else {
					frgMSItem.backgroundSubtract(bkgMSItem, 0, 0, 0, 0);
				}
			}
			Application.mainWindow.activeDocument.deletePages(backPage);
			frgMSItem.update();
			frgMSItem.selectionMode = MassItem.SpectrumSelMode.Peak;
			tic = frgMSItem.plot();
			peaks = tic.peaks;
			if (!tic.peaks) {
				errors += qsTr("No peaks found in TIC {0}\n").format(frgMSItem.datasetFileName);
				continue;
			}
			aux = this;
			max = 0;
			method.totalArea = 0;

			frgMSItem.selectSpectra(tic, peaks[0].center.x);

			if (frgMSItem.plotsCount < 2) {
				MessageBox.critical(qsTr("Chromatogram subtraction can't be applied to file {0} because background and foreground are the same spectrum.").format(aForeFiles[i]), MessageBox.Ok);
				Application.mainWindow.activeDocument.close();
			} else {
				try {
					for (j = 0; j < peaks.length && !progress.wasCanceled; j++) {
						if (peaks[j].area > peaks[max].area) {
							max = j;
						}
						method.totalArea += peaks[j].area;//Calculate Total Area
						if (this.getPeakValues(frgMSItem, peaks[j]) !== undefined) {
							method.vector.push(this.getPeakValues(frgMSItem, peaks[j]));
						} else {
							this.log += ("\n No peaks found in " + String.fromCharCode(65 + methods));
							break;
						}
					}
					fileName = aResDir + "/Method" + String.fromCharCode(65 + methods) + ".mnova";
					serialization.save(fileName, "mnova"); // save MNova file format

					methods++;
					Application.mainWindow.activeDocument.close();
					if (!progress.wasCanceled && method.vector.length !== 0) {
						method.vector[max].type = "Parent";
						methodsTable.push(method);
					}
				} catch (ERR) {
					print(ERR);
				}
			}
		}
		if (!progress.wasCanceled) {
			Application.mainWindow.newDocument();
			this.createReports();
		}
	} finally {
		progress.close();
		if (errors !== "") {
			completeDate = Str.date2ISOString(new Date());
			completeDate = completeDate.replace(new RegExp("\\:", 'g'), "_");
			dir = new Dir(aResDir);
			f = new File(dir.filePath(completeDate + "_ErrorsLog.txt"));
			f.open(File.WriteOnly);
			s = new TextStream(f);
			s.write(errors);
			s.flush();
			f.close();
		}
	}
};

ChromatogramSubtractor.prototype.getPeakValues = function (massItem, peak) {
	"use strict";
	var peaks, max, i, ion,
		peakValues = {};

	if (massItem === undefined) {
		MessageBox.critical("No MS found");
		return;
	}

	massItem.selectSpectra(massItem.plot(), peak.center.x);
	massItem.update();

	peakValues.RT = peak.center.x;
	peakValues.area = peak.area;

	peaks = massItem.plot(1).peaks;
	max = 0;
	if (peaks !== undefined && peaks.length !== 0) {
		for (i = 0; i < peaks.length; i++) {
			if (peaks[i].center.y > peaks[max].center.y) {
				max = i;
			}
		}
		ion = peaks[max].center.x;
		peakValues.ion = ion.toFixed(0);
		peakValues.type = "Impurity";
		return peakValues;
	} else {
		return;
	}
};

ChromatogramSubtractor.prototype.createReports = function () {
	"use strict";
	var title, header, table, i, parentIon, j, row, impurity, relativeArea, parentAverage, log,
		precision = this.aPrecision,
		methodsTable = this.methodsTable,
		impuritiesTable = this.impuritiesTable,
		drawnItems = [],
		parentAverageSum = 0;

	for (i = 0; i < methodsTable.length; i++) {
		title = [];
		header = [];
		table = [];

		title.push("Method " +  String.fromCharCode(65 + i));
		header.push("Peak   ");
		header.push("Retention Time ");
		header.push("Area Percent   ");
		header.push("Key Ion");

		table.push(title);
		table.push(header);
		parentIon = "";

		//Loop to draw method tables

		for (j = 0; j < methodsTable[i].vector.length; j++) {
			//Create a j row for i method table
			row = [];
			row.push(this.getPeakName(methodsTable[i].vector[j], i));
			row.push(methodsTable[i].vector[j].RT.toFixed(precision));
			row.push((methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea).toFixed(precision));
			row.push(methodsTable[i].vector[j].ion);
			table.push(row);

			//Calculate sum of areas for every method

			if (methodsTable[i].vector[j].type === "Parent") {
				//Treatment for the parent compound
				parentAverageSum += (methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea);
				parentIon = methodsTable[i].vector[j].ion;
			} else {
				//Treatment in case of impurity
				impurity = (this.getPeakName(methodsTable[i].vector[j], i).substring(this.getPeakName(methodsTable[i].vector[j], i).indexOf(" ")) - 1);
				relativeArea = methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea;

				if ((relativeArea) > impuritiesTable[impurity].area) {
					//New cluster representant impurity found
					impuritiesTable[impurity].area = relativeArea;
					impuritiesTable[impurity].method = i;
				}
			}
		}
		drawnItems.push(Application.draw.text(Str.rowsToHtmlTable(table, header.length), true));
	}

	//Final Purity table: Table with all detected impurities included at their highest peak areas and the arithmetic mean of the percent area of the parent compound.

	title = [];
	header = [];
	table = [];
	log = [];

	title.push("Final Purity Table");
	log.push(this.log);
	header.push("Peak	");
	header.push("Area Percent	");
	header.push("Key Ion");

	table.push(title);
	table.push(log);
	table.push(header);

	//Create impurity rows
	for (i = 0; i < impuritiesTable.length; i++) {
		row = [];
		row.push(impuritiesTable[i].name);
		row.push(impuritiesTable[i].area.toFixed(precision));
		row.push(impuritiesTable[i].ion);
		table.push(row);
	}

	//Create parent compound row
	row = [];
	row.push("Parent");
	parentAverage = (parentAverageSum / methodsTable.length);
	row.push(parentAverage.toFixed(precision));
	row.push(parentIon);
	table.push(row);

	drawnItems.push(Application.draw.text(Str.rowsToHtmlTable(table, header.length), true));

	for (i = 1; i < drawnItems.length; i++) {
		drawnItems[i].translate(drawnItems[i - 1].right - drawnItems[i].left, 0);
		drawnItems[i].update();
	}

	Application.mainWindow.activeDocument.update();
};

ChromatogramSubtractor.prototype.getPeakName = function (peak, methodNumber) {
	"use strict";
	var found, i, maxPeak, node,
		impuritiesTable = this.impuritiesTable;

	if (peak.type === "Parent") {
		return peak.type;
	} else {
		found = false;
		i = 0;
		maxPeak = 0;
		while (i < impuritiesTable.length && !found) {
			if (peak.ion === impuritiesTable[i].ion) {
				found = true;
			} else {
				i++;
			}
		}

		if (found) {
			return impuritiesTable[i].name;
		} else {
			//New impurity found. Add to impurities table and return value.
			node = {};
			node.ion = peak.ion;
			node.name = "Impurity " + (impuritiesTable.length + 1);
			node.area = 0;
			node.method = methodNumber;
			impuritiesTable.push(node);
			return node.name;
		}
	}
};

if (this.MnUi && MnUi.scripts_mass) {
	MnUi.scripts_mass.scripts_mass_ChromatogramSubtraction = ChromatogramSubtraction;
}
